package com.swak.fx.support.ui;

import static java.util.Objects.requireNonNull;

import java.util.List;

import javafx.animation.FadeTransition;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import lombok.Getter;
import lombok.Setter;

/**
 * UIPopup.
 * 
 * @author 618lf
 */
public class UIPopup extends Popup implements UILifecycle {

	// ================================================================================
	// Properties
	// ================================================================================
	private final String STYLE_CLASS = "ui-popup";
	private final String STYLESHEET = UI.load("css/UIPopup.css");

	private Window ownerWindow;

	@Getter
	private final StackPane root;

	@Setter
	private boolean ownerWindowBlurClosed = true; // 所属窗口失去焦点后，弹出框关闭
	@Setter
	private boolean ownerWindowClickClosed = true;// 所属窗口失去点击，弹出框关闭

	public UIPopup() {
		this.root = new StackPane();
		this.init();
	}

	@Override
	public void initializeParts() {
		this.getContent().add(root);
		this.root.getStylesheets().add(STYLESHEET);
		this.root.getStyleClass().add(STYLE_CLASS);
	}

	@Override
	public void setupValueChangedListeners() {
		this.contentNodeProperty().addListener((ob, o, n) -> {
			this.root.getChildren().remove(o);
			this.root.getChildren().add(n);
			if (o != null && o instanceof UIBorderPane) {
				((UIBorderPane) o).arrowLocationProperty().unbindBidirectional(this.locationProperty());
			}
			if (n instanceof UIBorderPane) {
				((UIBorderPane) n).arrowLocationProperty().bindBidirectional(this.locationProperty());
			}
		});
	}

	@Override
	public List<String> getStylesheets() {
		return root.getStylesheets();
	}

	public final StringProperty styleProperty() {
		return root.styleProperty();
	}

	public final void setStyle(String value) {
		styleProperty().set(value);
	}

	public final ObservableList<String> getStyleClass() {
		return root.getStyleClass();
	}

	public final DoubleProperty prefHeightProperty() {
		return root.prefHeightProperty();
	}

	public final DoubleProperty setPrefHeight(double height) {
		root.setPrefHeight(height);
		return prefHeightProperty();
	}

	public final DoubleProperty prefWidthProperty() {
		return root.prefWidthProperty();
	}

	public final DoubleProperty setPrefWidth(double weight) {
		root.setPrefWidth(weight);
		return prefWidthProperty();
	}

	public final void show(Node owner) {
		this.show(owner, 0);
	}

	public final void show(Node owner, double offset) {
		requireNonNull(owner);

		Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
		double popupWidth = this.root.prefWidth(-1);
		double popupHeight = this.root.prefHeight(-1);

		double anchorX = 0;
		double anchorY = 0;

		switch (getLocation()) {
		case RIGHT_BOTTOM:
			anchorX = bounds.getMinX() - popupWidth - offset;
			anchorY = bounds.getMaxY() - popupHeight;
			break;
		case RIGHT_CENTER:
			anchorX = bounds.getMinX() - popupWidth - offset;
			anchorY = bounds.getCenterY() - popupHeight * 0.5;
			break;
		case RIGHT_TOP:
			anchorX = bounds.getMinX() - popupWidth - offset;
			anchorY = bounds.getMinY();
			break;
		case BOTTOM_LEFT:
			anchorX = bounds.getMinX();
			anchorY = bounds.getMinY() - popupHeight - offset;
			break;
		case BOTTOM_CENTER:
			anchorX = bounds.getCenterX() - popupWidth * 0.5;
			anchorY = bounds.getMinY() - popupHeight - offset;
			break;
		case BOTTOM_RIGHT:
			anchorX = bounds.getMaxX() - popupWidth;
			anchorY = bounds.getMinY() - popupHeight - offset;
			break;
		case LEFT_BOTTOM:
			anchorX = bounds.getMaxX() + offset;
			anchorY = bounds.getMaxY() - popupHeight;
			break;
		case LEFT_CENTER:
			anchorX = bounds.getMaxX() + offset;
			anchorY = bounds.getCenterY() - popupHeight * 0.5;
			break;
		case LEFT_TOP:
			anchorX = bounds.getMaxX() + offset;
			anchorY = bounds.getMinY();
			break;
		case TOP_LEFT:
			anchorX = bounds.getMinX();
			anchorY = bounds.getMaxY() - offset;
			break;
		case TOP_CENTER:
			anchorX = bounds.getCenterX() - popupWidth * 0.5;
			anchorY = bounds.getMaxY() - offset;
			break;
		case TOP_RIGHT:
			anchorX = bounds.getMaxX() - popupWidth;
			anchorY = bounds.getMaxY() - offset;
			break;
		case CENTER:
			anchorX = bounds.getCenterX() - popupWidth * 0.5;
			anchorY = bounds.getCenterY() - popupHeight * 0.5;
			break;
		}
		this.show(owner, anchorX, anchorY);
	}

	@Override
	public final void show(Node owner, double x, double y) {
		this.show(owner, x, y, getFadeInDuration());
	}

	private final void show(Node owner, double x, double y, Duration fadeInDuration) {
		super.show(owner, x, y);
		this.showInOwnerWindow(owner.getScene().getWindow());
	}

	public final void show(Window owner, double offset) {
		double popupWidth = this.root.prefWidth(-1);
		double popupHeight = this.root.prefHeight(-1);

		double anchorX = 0;
		double anchorY = 0;

		switch (getLocation()) {
		case TOP_CENTER:
			anchorX = owner.getX() + owner.getWidth() / 2 - popupWidth / 2;
			anchorY = owner.getY() + offset;
			break;
		case RIGHT_CENTER:
			anchorX = owner.getX() + owner.getWidth() + offset;
			anchorY = owner.getY() + owner.getHeight() / 2 - popupHeight / 2;
			break;
		default:
			anchorX = owner.getX() + owner.getWidth() / 2 - popupWidth / 2;
			anchorY = owner.getY() + owner.getHeight() / 2 - popupHeight / 2 + offset;
			break;
		}

		// 提供后续可以纠正的方法
		this.showWithWindow(owner, anchorX, anchorY, popupWidth, popupHeight);
	}

	private void showWithWindow(Window owner, double anchorX, double anchorY, double popupWidth, double popupHeight) {
		Rectangle2D bounds = this.rectifyRectangle2D(owner, anchorX, anchorY, popupWidth, popupHeight);
		double rectifyAnchorX = bounds == null ? anchorX : bounds.getMinX();
		double rectifyAnchorY = bounds == null ? anchorY : bounds.getMinY();
		double rectifyWidth = bounds == null ? popupWidth : bounds.getWidth();
		double rectifyHeight = bounds == null ? popupHeight : bounds.getHeight();
		this.setPrefWidth(rectifyWidth);
		this.setPrefHeight(rectifyHeight);
		this.showInOwnerWindow(owner);
		super.show(owner, rectifyAnchorX, rectifyAnchorY);
	}

	protected Rectangle2D rectifyRectangle2D(Window window, double anchorX, double anchorY, double width,
			double heigth) {
		return null;
	}

	@Override
	public final void show(Window ownerWindow, double anchorX, double anchorY) {
		this.showInOwnerWindow(ownerWindow);
		super.show(ownerWindow, anchorX, anchorY);
	}

	private void showInOwnerWindow(Window ownerWindow) {
		if (this.ownerWindow != null) {
			this.unRegisterEventsAndListeners();
		}
		this.ownerWindow = ownerWindow;
		if (!isShowing()) {
			if (isAnimated()) {
				showFadeInAnimation(getFadeInDuration());
			}
		}
		this.registerEventsAndListeners();
	}

	private void showFadeInAnimation(Duration fadeInDuration) {
		Node skinNode = getRoot();
		skinNode.setOpacity(0);
		FadeTransition fadeIn = new FadeTransition(fadeInDuration, skinNode);
		fadeIn.setFromValue(0);
		fadeIn.setToValue(1);
		fadeIn.play();
	}

	private void hideFadeInAnimation(Duration fadeOutDuration) {
		Node skinNode = getRoot();
		FadeTransition fadeOut = new FadeTransition(fadeOutDuration, skinNode);
		fadeOut.setFromValue(skinNode.getOpacity());
		fadeOut.setToValue(0);
		fadeOut.setOnFinished(evt -> super.hide());
		fadeOut.play();
	}

	public final void hide(Duration fadeOutDuration) {
		if (this.ownerWindow != null) {
			this.unRegisterEventsAndListeners();
		}

		if (fadeOutDuration == null) {
			fadeOutDuration = DEFAULT_FADE_DURATION;
		}

		if (isShowing()) {
			if (isAnimated()) {
				this.hideFadeInAnimation(fadeOutDuration);
			} else {
				super.hide();
			}
		}
	}

	private void registerEventsAndListeners() {
		ownerWindow.xProperty().addListener(weakXListener);
		ownerWindow.yProperty().addListener(weakYListener);
		ownerWindow.widthProperty().addListener(weakHideListener);
		ownerWindow.heightProperty().addListener(weakHideListener);
		if (this.ownerWindowBlurClosed) {
			ownerWindow.focusedProperty().addListener(hideListener);
		}
		if (this.ownerWindowClickClosed) {
			ownerWindow.addEventFilter(MouseEvent.MOUSE_CLICKED, closePopOverOnOwnerWindowClicked);
		}
		ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closePopOverOnOwnerWindowClose);
		ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopOverOnOwnerWindowClose);
		ownerWindow.addEventFilter(KeyEvent.ANY, maskOwnerWindowEntryEvent);
	}

	private void unRegisterEventsAndListeners() {
		ownerWindow.xProperty().removeListener(weakXListener);
		ownerWindow.yProperty().removeListener(weakYListener);
		ownerWindow.widthProperty().removeListener(weakHideListener);
		ownerWindow.heightProperty().removeListener(weakHideListener);
		ownerWindow.focusedProperty().removeListener(hideListener);
		ownerWindow.removeEventFilter(KeyEvent.ANY, maskOwnerWindowEntryEvent);
		ownerWindow.removeEventFilter(MouseEvent.MOUSE_CLICKED, closePopOverOnOwnerWindowClicked);
		ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, closePopOverOnOwnerWindowClose);
		ownerWindow.removeEventFilter(WindowEvent.WINDOW_HIDING, closePopOverOnOwnerWindowClose);
	}

	private final ObjectProperty<UI.Locations> location = new SimpleObjectProperty<>(this, "location", //$NON-NLS-1$
			UI.Locations.CENTER);

	public final ObjectProperty<UI.Locations> locationProperty() {
		return location;
	}

	public final UI.Locations getLocation() {
		return locationProperty().get();
	}

	private static final Duration DEFAULT_FADE_DURATION = Duration.seconds(.2);
	private final SimpleBooleanProperty animated = new SimpleBooleanProperty(true);
	private final ObjectProperty<Duration> fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION);
	private final ObjectProperty<Duration> fadeOutDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION);

	public final ObjectProperty<Duration> fadeInDurationProperty() {
		return fadeInDuration;
	}

	public final ObjectProperty<Duration> fadeOutDurationProperty() {
		return fadeOutDuration;
	}

	public final Duration getFadeInDuration() {
		return fadeInDurationProperty().get();
	}

	public final Duration getFadeOutDuration() {
		return fadeOutDurationProperty().get();
	}

	public final BooleanProperty animatedProperty() {
		return animated;
	}

	public final boolean isAnimated() {
		return animatedProperty().get();
	}

	private InvalidationListener hideListener = new InvalidationListener() {
		@Override
		public void invalidated(Observable observable) {
			hide(Duration.ZERO);
		}
	};
	private WeakInvalidationListener weakHideListener = new WeakInvalidationListener(hideListener);
	private ChangeListener<Number> xListener = new ChangeListener<Number>() {
		@Override
		public void changed(ObservableValue<? extends Number> value, Number oldX, Number newX) {
			setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue()));
		}
	};
	private WeakChangeListener<Number> weakXListener = new WeakChangeListener<>(xListener);
	private ChangeListener<Number> yListener = new ChangeListener<Number>() {
		@Override
		public void changed(ObservableValue<? extends Number> value, Number oldY, Number newY) {
			setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue()));
		}
	};
	private WeakChangeListener<Number> weakYListener = new WeakChangeListener<>(yListener);
	private final EventHandler<KeyEvent> maskOwnerWindowEntryEvent = event -> event.consume();
	private final EventHandler<MouseEvent> closePopOverOnOwnerWindowClicked = event -> hide(Duration.ZERO);
	private final EventHandler<WindowEvent> closePopOverOnOwnerWindowCloseLambda = event -> hide(Duration.ZERO);
	private final WeakEventHandler<WindowEvent> closePopOverOnOwnerWindowClose = new WeakEventHandler<>(
			closePopOverOnOwnerWindowCloseLambda);

	private final ObjectProperty<Node> contentNode = new SimpleObjectProperty<Node>(this, "contentNode") { //$NON-NLS-1$
		@Override
		public void setValue(Node node) {
			if (node == null) {
				throw new IllegalArgumentException("content node can not be null"); //$NON-NLS-1$
			}
		};
	};

	public final ObjectProperty<Node> contentNodeProperty() {
		return contentNode;
	}

	public final Node getContentNode() {
		return contentNodeProperty().get();
	}

	public final void setContentNode(Node content) {
		contentNodeProperty().set(content);
	}
}